跳到主要内容

07 SCI UART串口通信实验

在这里插入图片描述

开发板官网https://www.edevkit.com/)

本系列教程目录:


SCI UART 串口通信实验

本章目标:

  • 熟悉 RASC 配置 SCI 串口的操作使用
  • 掌握串口通信 API 的使用
  • 掌握串口中断回调的使用

硬件设计

本实验使用开发板的 SCI UART 串口,通过 USB 转串口连接电脑,实现数据的收发通信。

软件设计

新建工程

复制一份“04 GPIO-led”修改文件夹名“07 SCI-UART_Recv_send”,双击打开 EBF_RA6M5.uvprojx 工程,通过 Keil 的新建文件新建 uart.cuart.h,保存到目录下的 \src 文件夹下。

FSP 配置

使用 RASC 配置 UART 时,需要进行两方面的配置:引脚配置、UART 本身的配置。

首先进行引脚的配置,先打开原理图确认使用哪一个 UART,比如引脚使用 TXD0/RXD0、TXD1/RXD1 等。

Keil 界面选择“Tools”→“RA Smart Configurator”打开 RASC 界面。

在 FSP 配置界面里面点开“Pins”→“Peripherals”→“SCI”→“SCI0”,在“Operation Mode”选择“Asynchronous UART”,TXD0、RXD0 选择电路原理图设计的引脚。

插图01

①选择“Pins”,包含所有 IO 和外围设备。

②选择“Peripherals”外围设备。

③选择 SCI 的 SCI0,串口 0。

④选择引脚组别,刚好用的 P410、P411,属于 B 组,混搭就选择“Mixed”。

⑤“operation Mode”选择“Asynchronous UART”使能SCI。

⑥选择TXD0,RXD0,发送接收引脚,根据电路图配置。

引脚通道配置完,还需要添加串口的Stack模块。

插图02

①选择“Stack”。

②选择“New Stack”,添加外设模块。

③选择“Connectivity”模块。

④选择UART功能,点击选择新建一个模块。

插图03

①新建了一个“g_uart0 UART”模块。

②点击这个模块,显示“Properties”属性界面。

配置名字(name)、通道(Channel)、回调函数(Callback)名字即可,引脚(Pins)、波特率(Baud Rate)等其他的属性按照默认的配置即可。

插图04

属性描述
Name名字,根据读者需求设置即可。
Channel通道,根据SCI号设置即可,例如实验使用SCI0,则这里配置为通道0。
Data Bits每个字(word)的比特(bit)数,默认为 8bits
Parity校验模式,可选择“Odd”奇校验,“Even”偶校验或“None”无校验。
Stop Bits停止位,可选 1 或 2bit。
Baud Rate波特率
Baud Rate Modulation波特率调制,通过调整时钟周期,以减少波特率与实际波特率之间的误差。
Max Error(%)计算波特率时允许的最大百分比误差。
Callback回调函数的名字,根据用户需求设置即可。
Receive Interrupt Priority接收中断优先级
Transmit Data Empty Interrupt Priority发送数据空中断优先级
Transmit End Interrupt Priority发送完成中断优先级
Error Interrupt Priority错误中断优先级

使用 printf 函数时,需要使用到堆,默认情况下堆的大小为 0,需要修改堆的大小。可以在 FSP 配置界面中的“BSP”属性栏的“RA Common”中通过修改“Heap size”来设置堆区大小。这里需要设置为 8 的整数倍,对于 RA6M5 推荐至少为 4K(0x1000)。

插图05

配置完成之后可以按下快捷键“Ctrl + S”保存, 最后点击 “Generate Project Content” 按钮,让软件自动生成配置代码即可。

  1. 引脚配置信息

引脚配置还是在 pin_data.c 文件里生成。

const ioport_pin_cfg_t g_bsp_pin_cfg_data[] = {
...省略
{
.pin = BSP_IO_PORT_04_PIN_10,
.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_SCI0_2_4_6_8)
},
{
.pin = BSP_IO_PORT_04_PIN_11,
.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_SCI0_2_4_6_8)
},
};

pin_cfg 参数配置为 IOPORT_CFG_PERIPHERAL_PIN,也就是复用连接到芯片的其它模块而非 GPIO;

参数 IOPORT_PERIPHERAL_SCI0_2_4_6_8 表示它们被连接到 SCI 模块。

  1. UART配置信息

UART 模块信息会在 hal_data.c 文件里生成。在 RASC 里指定了 UART 使用哪个 SCI 通道、指定了它的数据格式(数据位/校验位/停止位)、指定了波特率等信息,这些配置信息都被放入一个 uart_cfg_t 结构体,里面的内容跟配置时选择的参数一致。

const uart_cfg_t g_uart0_cfg =
{
.channel = 0,
.data_bits = UART_DATA_BITS_8,
.parity = UART_PARITY_OFF,
.stop_bits = UART_STOP_BITS_1,
.p_callback = uart0_callback,
.p_context = NULL,
.p_extend = &g_uart0_cfg_extend,
...省略
};

结构体 g_uart0_cfg 里引用到了另一个结构体 g_uart0_cfg_extend,里面含有时钟、FIFO、流量控制等信息。

const sci_uart_extended_cfg_t g_uart0_cfg_extend =
{
.clock = SCI_UART_CLOCK_INT,
.rx_edge_start = SCI_UART_START_BIT_FALLING_EDGE,
.noise_cancel = SCI_UART_NOISE_CANCELLATION_DISABLE,
.rx_fifo_trigger = SCI_UART_RX_FIFO_TRIGGER_MAX,
.p_baud_setting = &g_uart0_baud_setting,
.flow_control = SCI_UART_FLOW_CONTROL_RTS,
...省略
};

引用了另一个结构体 g_uart0_baud_setting。在 RASC 里配置了波特率为 115200,RASC 会计算出各个寄存器的值,存放在结构体 g_uart0_baud_setting 里。

baud_setting_t g_uart0_baud_setting =
{
/* Baud rate calculated with 0.469% error. */
.semr_baudrate_bits_b.abcse = 0,
.semr_baudrate_bits_b.abcs = 0,
.semr_baudrate_bits_b.bgdm = 1,
.cks = 0,
.brr = 53,
.mddr = (uint8_t) 256,
.semr_baudrate_bits_b.brme = false
};
  1. 中断回调函数

操作一个 UART 引脚时,要先打开它(open),open 函数会配置 UART;然后再调用 read、write 函数读、写串口。需要注意的是,sci_uart 模块里的读、写函数,只是“启动”读、写功能,这些函数返回时并不表示读、写已经完成。后续的读、写操作是由中断函数实现的。在传输过程中,中断函数会调用回调函数来处理各种状态(传输完成?出错?)。

回调函数原型在 hal_data.h 中定义。

void uart0_callback(uart_callback_args_t * p_args);

p_args 参数是 uart_callback_args_t 结构体类型。

typedef struct st_uart_callback_arg
{
uint32_t channel; ///< Device channel number
uart_event_t event; ///< Event code
/** Contains the next character received for the events UART_EVENT_RX_CHAR, UART_EVENT_ERR_PARITY,
* UART_EVENT_ERR_FRAMING, or UART_EVENT_ERR_OVERFLOW. Otherwise unused. */
uint32_t data;
void const * p_context; ///< Context provided to user during callback
} uart_callback_args_t;

参数里的 event 表示原因,它是一个 uart_event_t 枚举类型。

/** UART Event codes */
typedef enum e_sf_event
{
UART_EVENT_RX_COMPLETE = (1UL << 0), //read函数启动接收后,接收完毕
UART_EVENT_TX_COMPLETE = (1UL << 1), //发送完毕
UART_EVENT_RX_CHAR = (1UL << 2), //未用read启动接收但接收到了数据
UART_EVENT_ERR_PARITY = (1UL << 3), //校验错误
UART_EVENT_ERR_FRAMING = (1UL << 4), // < Mode fault error event
UART_EVENT_ERR_OVERFLOW = (1UL << 5), // FIFO溢出
UART_EVENT_BREAK_DETECT = (1UL << 6), // < Break detect error event
UART_EVENT_TX_DATA_EMPTY = (1UL << 7), //最后一个字节已经发送出去了,但是还没发送完毕
} uart_event_t;

对于接收,有 2 个 event:UART_EVENT_RX_COMPLETEUART_EVENT_RX_CHAR。前者被调用的时机是:调用 read 后接收到所有数据时;后者被调用的时机是:未调用 read 函数,但是接收到了数据。

对于发送,有 2 个 event:UART_EVENT_TX_DATA_EMPTYUART_EVENT_TX_COMPLETE。以发送 2 个字节的数据为例,前者被调用的时机是:第 1 个字节已经成功发送完毕,第 2 个字节已经被移入发送器但是还没发送完毕;后者被调用的时机是:这 2 个字节都发送完毕。前者先被调用,在“最后一个字节被发送,但是未发送完毕”时被调用;后者在“最后一个字节也完全发送完毕后”被调用。

程序设计

实现串口的数据收发,通过 USB 串口连接开发板和电脑,电脑端使用串口调试助手给开发板发送数据,开发板把接收的数据发送回电脑端。

  1. 打开 UART 设备
R_SCI_UART_Open (&g_uart0_ctrl, &g_uart0_cfg);
  1. 发送数据
fsp_err_t R_SCI_UART_Write (uart_ctrl_t * const p_api_ctrl, uint8_t const * const p_src, uint32_t const bytes);

串口初始化完成之后,我们可以直接使用 R_SCI_UART_Write 函数来将字符串写入到串口输出。

参数 p_src 指向要写入的字符串首地址

参数 bytes 为传入的要写入的字符的数目

  1. 读取数据
fsp_err_t R_SCI_UART_Read(uart_ctrl_t * const p_api_ctrl, uint8_t * const p_dest, uint32_t const bytes);

参数 p_dest 指向要保存的数组首地址

参数 bytes 为传入的要读取的字符的数目

在使用 R_SCI_UART_Write 函数需要注意的一些事项

对于普通的读写操作,可以在回调函数里设置状态标记,用来表示读、写是否已经完成。这样,使用 read、write 函数启动读、写操作后,就可以轮询这些状态以等待操作完成。

static volatile int g_uart0_tx_complete = 0;
static volatile int g_uart0_rx_complete = 0;

void uart0_callback(uart_callback_args_t * p_args)
{
switch (p_args->event)
{
case UART_EVENT_TX_COMPLETE:
{
g_uart0_tx_complete = 1;
break;
}
case UART_EVENT_RX_COMPLETE:
{
g_uart0_rx_complete = 1;
break;
}
default:
{
break;
}
}
}

void uart0_wait_for_tx(void)
{
while (!g_uart0_tx_complete);
g_uart0_tx_complete = 0;
}
void uart0_wait_for_rx(void)
{
while (!g_uart0_rx_complete);
g_uart0_rx_complete = 0;
}

在回调函数里设置全局变量 g_uart0_tx_complete,表示发送完毕。用户程序在调用 write 启动 UART 发送后,就可以调用第 25 行的 uart0_wait_for_tx 以等待发送完毕。

在回调函数里设置全局变量 g_uart0_rx_complete,表示接收完毕。用户程序在调用 read 启动 UART 接收后,就可以调用第 30 行的 uart0_wait_for_rx 以等待接收完毕。

重定向 printf 输出到串口

调试程序时,希望从串口打印一些数据或格式化的字符串查看程序运行情况,使用 R_SCI_UART_Write() 函数只能发送字节数据,很不方便,可以添加重定向 printf 代码,重写 fputc() 函数,通过串口发送格式化字符串。

/* 重定向 printf 输出 */
#if defined __GNUC__ && !defined __clang__
int _write(int fd, char *pBuffer, int size); //防止编译警告
int _write(int fd, char *pBuffer, int size)
{
(void)fd;
R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)pBuffer, (uint32_t)size);
while(uart_send_complete_flag == false);
uart_send_complete_flag = false;

return size;
}
#else
int fputc(int ch, FILE *f)
{
(void)f;
R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)&ch, 1);
while(uart_send_complete_flag == false);
uart_send_complete_flag = false;

return ch;
}
#endif

程序编写

uart.h 头文件里定义中断初始化函数,数据发送函数,接收扫描函数。

#ifndef __BSP_UART_H
#define __BSP_UART_H
#include "hal_data.h"
#include "stdio.h"

//初始化uart
void uart0_Init(void);

//封装发送指定长度数据
void uart_send_data(uint8_t *data, uint16_t len);

//扫描接收情况,超过8字节就把接收的数据发送回去
void uart_scan_rx(void);

#endif

uart.c 里实现初始化函数、中断回调函数,数据发送函数,接收扫描函数。

中断函数:处理数据的接收,一般需要接收一帧完整数据,就需要定义一个数组接收到一个字节就保存到数组。

数据发送函数:封装一个发送自定长度数据函数,发送数据后等待发送完成标识才表示发送完成。

接收扫描函数:放在循环中扫描接收的数据长度,达到8字节就把接收的数据发送回去。

#include "uart.h"

//初始化uart
void uart0_Init(void)
{
fsp_err_t err = FSP_SUCCESS;

err = R_SCI_UART_Open (&g_uart0_ctrl, &g_uart0_cfg);

assert(FSP_SUCCESS == err);
}

static volatile int g_uart0_tx_complete = 0;//发送完成标识
static volatile int g_uart0_rx_complete = 0;//接收完成标识

uint8_t rxBuf[32] = {0};//接收数据数组
uint16_t rxCnt = 0; //接收数据长度

// 串口中断回调
void uart0_callback (uart_callback_args_t * p_args)
{
switch (p_args->event)
{
case UART_EVENT_RX_CHAR://接收到一个字节
{
//没有接收满继续接受,接收满了不在接收
if(rxCnt < sizeof(rxBuf)) {
rxBuf[rxCnt++] = (uint8_t)p_args->data;
}
break;
}
case UART_EVENT_TX_COMPLETE://发送完成
{
g_uart0_tx_complete = 1;
break;
}
case UART_EVENT_RX_COMPLETE://接收完成
{
g_uart0_rx_complete = 1;
break;
}
default:
break;
}
}

//等待接收完成
void uart0_wait_for_tx(void)
{
while (!g_uart0_tx_complete);
g_uart0_tx_complete = 0;
}

//等待发送完成
void uart0_wait_for_rx(void)
{
while (!g_uart0_rx_complete);
g_uart0_rx_complete = 0;
}

//封装发送指定长度数据
void uart_send_data(uint8_t *data, uint16_t len)
{
//把串口接收到的数据发送回去
R_SCI_UART_Write(&g_uart0_ctrl, data, len);
//等待接收完成
uart0_wait_for_tx();
}

//扫描接收情况,超过8字节就把接收的数据发送回去
void uart_scan_rx(void)
{
if(rxCnt >= 8)
{
//发送
uart_send_data(rxBuf, rxCnt);

//清除接收的数据
rxCnt = 0;
memset(rxBuf,0,sizeof(rxBuf));
}
}

// 重定向 printf 输出
#if defined __GNUC__ && !defined __clang__
int _write(int fd, char *pBuffer, int size); //防止编译警告
int _write(int fd, char *pBuffer, int size)
{
(void)fd;
R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)pBuffer, (uint32_t)size);
while(uart_send_complete_flag == false);
uart_send_complete_flag = false;

return size;
}
#else
int fputc(int ch, FILE *f)
{
(void)f;
R_SCI_UART_Write(&g_uart0_ctrl, (uint8_t *)&ch, 1);
uart0_wait_for_tx();

return ch;
}
#endif

hal_entry.c 文件中添加头文件 uart.h,在 hal_entry 入口函数初始化 uart0,并添加 printf("uart init ok\r\n") 一条调试语句,循环中执行 uart_scan_rx() 扫描函数。

void hal_entry(void)
{
/* TODO: add your own code here */

led_Init(); // LED 初始化
uart0_Init();

printf("uart init ok\r\n");

while(1)
{
LED1_TOGGLE;
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS); //延时1秒

//扫描uart接收
uart_scan_rx();
}


#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}

下载验证

首先通过 USB 串口连接电脑和开发板,串口的 RX/TX 分别连接开发板的 TX/RX,交叉连接,电脑端打开串口调试助手,波特率设置 115200。

连接下载器,将程序编译并下载到开发板之后,按下复位按键来复位开发板,程序启动初始化完会打印一句“uart init ok”。

插图06

LED 闪烁,说明程序正常运行,在串口调试助手发送 16 进制 8 字节数据,在接收串口窗口收到相同数据。

插图07